//////////////////////////////////////////////
// main.cpp
//
//////////////////////////////////////////////

/// Includes ---------------------------------

// nkLog
#include <NilkinsLog/Loggers/ConsoleLogger.h>

// nkGraphics
#include <NilkinsGraphics/Encoders/Obj/ObjEncoder.h>

#include <NilkinsGraphics/Entities/Entity.h>

#include <NilkinsGraphics/Graph/Node.h>
#include <NilkinsGraphics/Graph/NodeManager.h>

#include <NilkinsGraphics/Log/LogManager.h>

// Needed to manipulate the higher level API
#include <NilkinsGraphics/Meshes/Utils/MeshUtils.h>
#include <NilkinsGraphics/Meshes/Utils/VertexComposition.h>
#include <NilkinsGraphics/Meshes/Utils/VertexData.h>

// Basis to work with meshes
#include <NilkinsGraphics/Meshes/Mesh.h>
#include <NilkinsGraphics/Meshes/MeshManager.h>

#include <NilkinsGraphics/RenderContexts/RenderContextDescriptor.h>
#include <NilkinsGraphics/RenderContexts/RenderContextManager.h>

#include <NilkinsGraphics/RenderQueues/RenderQueue.h>
#include <NilkinsGraphics/RenderQueues/RenderQueueManager.h>

#include <NilkinsGraphics/System.h>

// nkMemory
#include <NilkinsMemory/Containers/BufferCast.h>

// nkResources
#include <NilkinsResources/ResourceManager.h>

// Standards
#include <memory>

/// Internals --------------------------------

void prepareMeshFromCodeWithoutUtils ()
{
	// Create mesh through manager
	nkGraphics::Mesh* mesh = nkGraphics::MeshManager::getInstance()->createOrRetrieve("Mesh") ;

	// We can pack data on CPU, for this one we will use the vector from nkMaths
	// But we could use whatever format and packing we want
	// Constraint is that it needs to map to a format available in attributes, or be described using strides and offsets
	nkMemory::BufferCast<nkMaths::Vector> pointArray (3) ;
	pointArray[0] = nkMaths::Vector(-1.f, -1.f, 10.f) ;
	pointArray[1] = nkMaths::Vector(0.f, 1.f, 10.f) ;
	pointArray[2] = nkMaths::Vector(1.f, -1.f, 10.f) ;

	// Setup the layout describing this CPU data
	// We name our attributes using the Nilkins convention, but these could be named differently and mapped through the layout's annotations
	// Attribute in this case will auto compute its offset (0) and stride (16) from its format and order in the layout attribute array
	nkGraphics::MeshInputLayoutAttribute positionAttribute ("POSITION") ;
	positionAttribute._format = nkGraphics::R32G32B32A32_FLOAT ;

	// Prepare layout which is only formed by the position attribute
	nkGraphics::MeshInputLayout inputLayout ;
	inputLayout.addAttribute(positionAttribute) ;

	// And use them to populate mesh data
	mesh->addVertexBufferForward(pointArray.relinquishBufferOwnership()) ;
	mesh->setInputLayout(inputLayout) ;
	mesh->setVertexCount(3) ;

	// Prepare an index buffer to describe mesh surface
	nkMemory::BufferCast<unsigned int> indexArray (3) ;

	// Index data, forward is counter clockwise, we need to take it into account for back face culling
	indexArray[0] = 0 ;
	indexArray[1] = 2 ;
	indexArray[2] = 1 ;

	// Give information to the mesh
	mesh->setIndexBufferForward(indexArray.relinquishBufferOwnership()) ;
	mesh->setIndexCount(3) ;

	// Load the mesh now that everything is ready
	mesh->load() ;
}

void prepareMeshFromCodeWithUtils ()
{
	// Create a mesh by requesting the manager
	nkGraphics::Mesh* mesh = nkGraphics::MeshManager::getInstance()->createOrRetrieve("Mesh") ;

	// Create a buffer of 3 vertices to populate using the structure given
	nkMemory::BufferCast<nkGraphics::VertexData> pointArray (3) ;
	pointArray[0]._position = nkMaths::Vector(-1.f, -1.f, 10.f) ;
	pointArray[1]._position = nkMaths::Vector(0.f, 1.f, 10.f) ;
	pointArray[2]._position = nkMaths::Vector(1.f, -1.f, 10.f) ;

	// Prepare the composition we want, with position only
	nkGraphics::VertexComposition composition ;
	composition._hasPositions = true ;
	composition._hasColors = false ;
	composition._hasTexCoords = false ;
	composition._hasNormals = false ;
	composition._hasTangents = false ;
	composition._hasBinormals = false ;

	// Request translation of both structures into data we can feed to the mesh
	nkGraphics::PackedMeshData meshData = nkGraphics::MeshUtils::packIntoMeshData(pointArray, composition) ;

	// And fit into the mesh (stride will be computed automatically from layout, data being tightly packed)
	mesh->addVertexBufferForward(std::move(meshData._vertexBuffer)) ;
	mesh->setInputLayout(meshData._inputLayout) ;
	mesh->setVertexCount(3) ;

	// Prepare an index buffer to describe mesh surface
	nkMemory::BufferCast<unsigned int> indexArray (3) ;

	// Index data, forward is counter clockwise, we need to take it into account for back face culling
	indexArray[0] = 0 ;
	indexArray[1] = 2 ;
	indexArray[2] = 1 ;

	// Give information to the mesh
	mesh->setIndexBufferForward(indexArray.relinquishBufferOwnership()) ;
	mesh->setIndexCount(3) ;

	// Load the resource once our setup is good
	// It is possible to change the whole setup and reload if need be
	// Keep in mind that a resource doesn't need to be loaded to be used in rendering, but it will correspond to a no-op
	mesh->load() ;
}

void prepareMeshFromFilePath ()
{
	// Create mesh the alternative easy way, but lacking control over import
	nkGraphics::Mesh* mesh = nkGraphics::MeshManager::getInstance()->createOrRetrieve("Mesh") ;

	mesh->setPath("sphere.obj") ;

	mesh->load() ;
}

void prepareMeshFromFileEncoder ()
{
	// Another way of creating a mesh is by loading it from external data
	nkGraphics::Mesh* mesh = nkGraphics::MeshManager::getInstance()->createOrRetrieve("Mesh") ;

	// Load data from file
	nkMemory::String absPath = nkResources::ResourceManager::getInstance()->getAbsoluteFromWorkingDir("sphere.obj") ;
	nkMemory::Buffer objData = nkResources::ResourceManager::getInstance()->loadFileIntoMemory(absPath) ;

	// Request decoding, here we know the format so we address the ObjEncoder directly
	// We change some settings to alter the way the mesh is imported
	nkGraphics::ObjDecodeOptions objOptions ;
	objOptions._invertUvY = true ;
	objOptions._invertWindingOrder = true ;
	nkGraphics::DecodedData objDecoded = nkGraphics::ObjEncoder::decode(objData, objOptions) ;

	// We can then either fill the mesh manually through the decoded mesh data, or request the Utils to do it for us here
	// We know we won't use the decoded data later, so we request the use of memory forwarding during filling, rather than copying
	nkGraphics::MeshFillOptions fillOptions ;
	fillOptions._autoLoad = true ;
	fillOptions._dataFillType = nkGraphics::DATA_FILL_TYPE::FORWARD ;
	nkGraphics::MeshUtils::fillMeshFromDecodedData(objDecoded._meshData[0], mesh, fillOptions) ;
}

void addMeshToRenderQueue ()
{
	// Meshes are added to rendering by using render queues
	// By default, the queue number 0 is available, so we will use it
	nkGraphics::RenderQueue* rq = nkGraphics::RenderQueueManager::getInstance()->get(nkGraphics::RenderQueueManager::DEFAULT_RENDER_QUEUE) ;

	// Find back our mesh from the manager
	nkGraphics::Mesh* mesh = nkGraphics::MeshManager::getInstance()->createOrRetrieve("Mesh") ;

	// And prepare it by adding it to the render queue
	nkGraphics::Entity* ent = rq->addEntity() ;
	ent->setRenderInfo(nkGraphics::EntityRenderInfo(mesh, nullptr)) ;
}

void addEntityToGraph ()
{
	// Retrieve the queue we worked on, and find back the entity we created
	nkGraphics::RenderQueue* rq = nkGraphics::RenderQueueManager::getInstance()->get(nkGraphics::RenderQueueManager::DEFAULT_RENDER_QUEUE) ;
	nkGraphics::Entity* ent = rq->getEntity(0) ;

	// Nodes allow to put entities in a graph, and change their setup in the virtual world
	nkGraphics::Node* node = nkGraphics::NodeManager::getInstance()->createOrRetrieve("node") ;
	node->setPositionAbsolute(nkMaths::Vector(0.f, 0.f, 10.f)) ;

	ent->setParentNode(node) ;
}

/// Function ---------------------------------

int main ()
{
	// Prepare for logging
	std::unique_ptr<nkLog::Logger> logger = std::make_unique<nkLog::ConsoleLogger>() ;
	nkGraphics::LogManager::getInstance()->setReceiver(logger.get()) ;

	// To find back the data
	nkResources::ResourceManager::getInstance()->setWorkingPath("Data") ;

	// Initialize and create context with window
	if (!nkGraphics::System::getInstance()->initialize())
		return -1 ;

	nkGraphics::RenderContext* context = nkGraphics::RenderContextManager::getInstance()->createRenderContext(nkGraphics::RenderContextDescriptor(800, 600, false, true)) ;

	// Prepare the mesh we will show
	// Which mesh displayed can be chosen through the right function call (file or code)
	//prepareMeshFromCodeWithoutUtils() ;
	//prepareMeshFromCodeWithUtils() ;
	//prepareMeshFromFilePath() ;
	prepareMeshFromFileEncoder() ;
	addMeshToRenderQueue() ;
	addEntityToGraph() ;

	// Run and clean
	nkGraphics::System::getInstance()->run(context) ;
	nkGraphics::System::getInstance()->kill() ;
}